連結散布図#

時間とともに変化する量的変数が複数ある場合でも、折れ線グラフを利用することは可能です。 縦軸・横軸に量的変数の値を取り、マーカー付近に時点情報を付加することで、二つの量的変数の連続的な変化を表現します。 これを 連結散布図Connected Scatter )と呼びます。

マンガ雑誌巻号の年間平均ページ数と年間平均価格の推移を表現した連結散布図を例に説明します。 連結散布図では、直交する二つの 位置 スケール(上図「位置①」「位置②」)を用いて二つの量的変数(上図「平均ページ数」「平均価格」)を表現します。 加えて、時間的に隣接するマーカー同士を直線で結び、付近にそのテキスト(上図「1988」)を付与することで、軌跡であることを強調します。 解釈性を高めるため、マーカーの スケールを時間を表す量的変数と対応付けることもあります。

Plotlyでは、plotly.express.line()で連結散布図を作成できます。

# plotly.expressモジュールをpxという名前でインポート
# インタラクティブなプロット作成に特化したモジュール
import plotly.express as px

# データフレームdfをcol_textカラムの値で昇順にソート
# ignore_index=Trueオプションで新しいインデックスを割り当て直す
df = df.sort_values("col_text", ignore_index=True)

# px.line関数を使用して折れ線グラフを作成
# 'df'データフレームの'col_x'カラムをx軸に、'col_y'カラムをy軸に設定
# 'text'引数に'col_text'カラムを指定して、各点にテキスト情報を追加
# 作成した折れ線グラフは'fig'という変数に格納される
fig = px.line(df, x="col_x", y="col_y", text="col_text")

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsのインポート
# より詳細なグラフ作成機能を利用可能
# goという名前で参照可能
import plotly.graph_objects as go

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../data/gm/input")

# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = DIR_CM.parent / "output" / Path.cwd().parts[-1] / "connected"
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = DIR_AN.parent / "output" / Path.cwd().parts[-1] / "connected"
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = DIR_GM.parent / "output" / Path.cwd().parts[-1] / "connected"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# マンガ作品と原作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"

# アニメ各話に関するファイル
FN_AE = "an_ae.csv"

# アニメ作品と声優の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"

# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# 国内主要ゲームメーカーのプラットフォームとメーカー名の対応辞書
# キー: プラットフォーム名、値: メーカー名の略称
PF2MK = {
    "プレイステーション": "ソニー",
    "プレイステーション2": "ソニー",
    "プレイステーション・ポータブル": "ソニー",
    "プレイステーション3": "ソニー",
    "プレイステーションVita": "ソニー",
    "プレイステーション4": "ソニー",
    "ゲームアーカイブス": "ソニー",
    "SG-1000": "セガ",
    "SC-3000": "セガ",
    "SEGAマーク3": "セガ",
    "セガ・マスターシステム": "セガ",
    "メガドライブ": "セガ",
    "ゲームギア": "セガ",
    "セガサターン": "セガ",
    "ドリームキャスト": "セガ",
    "ファミリーコンピュータ": "任天堂",
    "ゲームボーイ": "任天堂",
    "スーパーファミコン": "任天堂",
    "NINTENDO64": "任天堂",
    "ゲームボーイアドバンス": "任天堂",
    "ニンテンドーゲームキューブ": "任天堂",
    "ニンテンドーDS": "任天堂",
    "ニンテンドー3DS": "任天堂",
    "Wii": "任天堂",
    "WiiU": "任天堂",
    "NintendoSwitch": "任天堂",
}
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"

関数#

以下では、本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def upsample_yearly_data(df: pd.DataFrame, col: str = None) -> pd.DataFrame:
    """
    指定されたDataFrameに対して年、オプションで指定されたカラム(col)の
    アップサンプリングを行い、存在しないデータを0で埋めたDataFrameを返す

    Parameters
    ----------
    df : pd.DataFrame
        アップサンプリングを行いたいDataFrame;'year'、オプションでcolのカラムを含むこと
    col : str, optional
        アップサンプリングに含める追加のカラム名

    Returns
    -------
    pd.DataFrame
        アップサンプリングされたDataFrame;存在しないデータは0で埋められる

    """
    # 最古と最新の年を特定する
    y_min, y_max = df["year"].min(), df["year"].max()

    # 指定された範囲の全ての年、オプションでcolの組み合わせを生成する
    if col and col in df.columns:
        # col列のユニークな要素を全て取得し、valsとして保存
        vals = df[col].unique()
        # year列, quarter列, col列の全ての組合せを保持するDataFrame
        df_all = pd.DataFrame(
            [(y, c) for y in range(y_min, y_max + 1) for c in vals],
            columns=["year", col],
        )
        # dfとマージする際に利用する列一覧
        cols_merge = ["year", col]
    else:
        # year列の全ての組合せを保持するDataFrame
        df_all = pd.DataFrame([y for y in range(y_min, y_max + 1)], columns=["year"])
        # dfとマージする際に利用する列一覧
        cols_merge = ["year"]

    # 元のDataFrameと組み合わせたDataFrameを作成し、存在しないデータは0で埋める
    df_new = df_all.merge(df, on=cols_merge, how="left").fillna(0)

    # cols_merge以外を整数型に変換(fillnaにより浮動小数点型になっているため)
    cols_fillna = [c for c in df_new.columns if c not in cols_merge]
    for c in cols_fillna:
        df_new[c] = df_new[c].astype(int)

    return df_new
Hide code cell content
def create_connectedplot(
    df: pd.DataFrame, x: str, y: str, text: str, color: str, **args
) -> Figure:
    """
    データフレームから連結散布図を作成する

    Parameters
    ----------
    df : pd.DataFrame
        グラフに使用するデータを含むpandasデータフレーム
    x : str
        x軸に使用するデータフレームの列名
    y : str
        y軸に使用するデータフレームの列名
    text : str
        プロット上で表示するテキストを含むデータフレームの列名
    color : str
        プロットのカラーマッピングに使用されるデータフレームの列名
        カラーバーのタイトルとしても使用される
    **args
        追加のキーワード引数。これらはPlotly Express関数に渡される

    Returns
    -------
    fig : plotly.graph_objs._figure.Figure
        生成されたPlotlyのFigureオブジェクト
    """

    # 折れ線グラフを使って連結散布図の基礎を作成する
    fig_line = px.line(df, x=x, y=y, text=text, **args)

    # 散布図を作成し、連結散布図のポイントとして折れ線グラフに追加する
    fig_scatter = px.scatter(df, x=x, y=y, color=color, **args)

    # 散布図のトレースを折れ線グラフに追加し、接続点を表現する
    for trace in fig_scatter.data:
        fig_line.add_trace(trace)

    # 折れ線グラフのスタイルを更新し、視覚的特徴を強化する
    fig_line.update_traces(
        line={"color": "grey"},
        marker={"size": 15, "line_width": 1, "opacity": 0.7},
        textposition="bottom right",
    )

    # グラフのレイアウトを更新し、カラーバーのタイトルを`color`引数に基づいて設定する
    fig_line.update_layout(coloraxis_colorbar=dict(title=color))

    return fig_line
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

マンガデータ#

雑誌巻号に関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)
Hide code cell content
# 雑誌の各巻号(miname)ごとにデータを集計
df_mi = (
    df_ce.groupby(["miname"])
    .agg(
        {
            "ccid": "nunique",  # 作品数:ユニークなccidの数
            "page_end": "max",  # 合計ページ数:page_endの最大値
            "date": "first",  # 発売日:dateの最初の値
            "price": "first",  # 価格:priceの最初の値
            "mcname": "first",  # 雑誌名:mcnameの最初の値
        }
    )
    .reset_index()
)

# 'date'列を日付型に変換することで、文字列や他の形式の日付データを日付型に変換できる
df_mi["date"] = pd.to_datetime(df_mi["date"])
# 'date'列から年を抽出して、新しい列 'year' を作成
df_mi["year"] = df_mi["date"].dt.year
Hide code cell content
# 'year'でグループ化し、'ccid', 'page_end', 'price'の平均値を計算
# 'reset_index()'を使って、集計後も 'year' 列をデータフレームに保持する
df_cm = df_mi.groupby("year")[["ccid", "page_end", "price"]].mean().reset_index()

# 列名を変更して、可視化した際により理解しやすい名前にする
df_cm = df_cm.rename(
    columns={
        "year": "発売年",
        "ccid": "平均マンガ作品数",
        "page_end": "平均ページ数",
        "price": "平均価格",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
発売年 平均マンガ作品数 平均ページ数 平均価格
0 1970 13.965116 282.569767 83.023256
1 1971 13.401961 283.029412 87.339901
2 1972 13.323529 285.960784 95.392157
3 1973 13.818627 295.328431 101.911765
4 1974 11.964824 238.608040 128.592965
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../data/cm/output/08/connected/cm.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均ページ数'、y軸には'平均価格'、テキストとマーカーの色には'発売年'を設定
fig = create_connectedplot(
    df_cm, x="平均ページ数", y="平均価格", text="発売年", color="発売年"
)

# 作成したグラフを表示する関数 'show_fig' を呼び出し
show_fig(fig)

上図は、マンガ雑誌巻号の平均ページ数と平均価格の推移を表現した連結散布図です。一年ごとに集計した平均値を採用しています。マーカーの色が暗いほど古く、明るいほど新しい発売年を表します。

雑誌巻号の平均価格については1970年から徐々に増加していることがわかりますが、平均ページ数については1974年から1976年頃まで一度減少しています。 ページ数の集計方法に原因がある[1]可能性があるため、念のため 平均マンガ作品数 と平均価格の関係も可視化してみましょう。

Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均マンガ作品数'、y軸には'平均価格'、テキストには'発売年'を設定
fig = create_connectedplot(
    df_cm, x="平均マンガ作品数", y="平均価格", text="発売年", color="発売年"
)

# 作成したグラフを表示する関数 'show_fig' を呼び出し
show_fig(fig)

上図は、マンガ雑誌巻号の平均マンガ作品数数と平均価格の推移を表現した連結散布図です。一年ごとに集計した平均値を採用しています。マーカーの色が暗いほど古く、明るいほど新しい発売年を表します。

平均マンガ作品数に関しても、1974年から1976年頃にかけて減少していたことがわかります。 平均ページ数の減少も考慮すると、これはデータ集計に起因する不整合ではなく 事実として マンガ雑誌巻号が薄くなったと判断する方が自然です。

この理由として考えられるのは、1973年の第四次中東戦争を機に始まった 第一次石油危機 [Yamakoshi, 1986]です。 1973年11月に各地でトイレットペーパーや洗剤等の物不足騒ぎが生じるほど社会不安が広がり、1974年度の経済成長率は戦後始初めてのマイナス成長を記録したとされています。 1975年-1976年を日本経済の回復局面とする分析結果[隆夫 and 内閣府経済社会総合研究所, 2011]と、上図で平均ページ数や平均マンガ作品数が回復し始めた時期も一致しています[2]

それでは、マンガ雑誌ごとに違いはあるのでしょうか? 今回はファセットをわけて連結散布図を作成してみましょう。

Hide code cell content
# 'year'と'mcname'でグループ化し、'ccid', 'page_end', 'price'の平均値を計算
# 'reset_index()'を使って、集計後も 'year' 列をデータフレームに保持する
df_cm2 = (
    df_mi.groupby(["year", "mcname"])[["ccid", "page_end", "price"]]
    .mean()
    .reset_index()
)

# 列名を変更して、可視化した際により理解しやすい名前にする
df_cm2 = df_cm2.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "year": "発売年",
        "ccid": "平均マンガ作品数",
        "page_end": "平均ページ数",
        "price": "平均価格",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_cm2.head()
発売年 マンガ雑誌名 平均マンガ作品数 平均ページ数 平均価格
0 1970 週刊少年サンデー 12.333333 284.190476 84.285714
1 1970 週刊少年ジャンプ 14.863636 286.590909 82.727273
2 1970 週刊少年チャンピオン 17.590909 285.272727 82.727273
3 1970 週刊少年マガジン 10.857143 273.904762 82.380952
4 1971 週刊少年サンデー 12.411765 281.666667 86.274510
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm2, DIR_OUT_CM, "cm2")
DataFrame is saved as '../../data/cm/output/08/connected/cm2.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均ページ数'、y軸には'平均価格'、テキストと配色には'発売年'を設定
# マンガ雑誌名でファセットを分け、2列で折り返すよう設定し、高さも調整
fig = create_connectedplot(
    df_cm2,
    x="平均ページ数",
    y="平均価格",
    text="発売年",
    color="発売年",
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
    height=600,
)

# ファセット(マンガ雑誌ごとの連結散布図)のタイトルを簡潔にする処理
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 連結散布図を表示
show_fig(fig)

上図は、マンガ雑誌巻号の平均ページ数と平均価格の推移を表現した連結散布図を、マンガ雑誌別に表示したものです。 一年ごとに集計した平均値を採用しています。 マーカーの色が暗いほど古く、明るいほど新しい発売年を表します。

他誌と比較して週刊少年マガジン1974年の平均ページ数の落ち込みが大きかったことがわかります。 また、全てのマンガ雑誌が同様に2000年頃に450ページ程度に到達していますが、それ以降、週刊少年チャンピオンのみ平均ページ数を増やしていないことがわかりました。

なお、図中のテキストが重複してしまって見づらい場合は、一部のマーカーのみにテキストを表示することで回避できることがあります。

Hide code cell content
# テキスト表示用の列を追加:発売年が5の倍数のとき以外はNoneを格納
df_cm2["text"] = df_cm2["発売年"].apply(lambda x: str(x) if x % 5 == 0 else None)
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均ページ数'、y軸には'平均価格'、テキストには'text'、配色には'発売年'を設定
# マンガ雑誌名でファセットを分け、2列で折り返すよう設定し、高さも調整
fig = create_connectedplot(
    df_cm2,
    x="平均ページ数",
    y="平均価格",
    text="text",
    color="発売年",
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
    height=600,
)

# ファセット(マンガ雑誌ごとの連結散布図)のタイトルを簡潔にする処理
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 連結散布図を表示
show_fig(fig)

上図は、マンガ雑誌巻号の平均ページ数と平均価格の推移を表現した連結散布図を、マンガ雑誌別に表示したものです。 一年ごとに集計した平均値を採用しています。 マーカーの色が暗いほど古く、明るいほど新しい発売年を表します。 マーカー上のテキストは、発売年が5倍数のときのみ表示しています。

テキストの表示頻度を下げることで、少しだけ見やすくなりました。

アニメデータ#

アニメ作品や声優に関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
df_ac_act = pd.read_csv(DIR_AN / FN_AC_ACT)

折れ線グラフでの例と同様、5話以上放送されたアニメ作品のみを可視化対象とします。

Hide code cell content
# acidごとにユニークなaeid数を集計し、n_ae列として格納
df_ac_nae = df_ae.groupby("acid")["aeid"].nunique().reset_index(name="n_ae")
# n_aeについて昇順でソートし、インデックスを更新
df_ac_nae = df_ac_nae.sort_values("n_ae", ignore_index=True)

# 5話以上のアニメ各話と紐付けられているacidを抽出し、acidsとして格納
acids = df_ac_nae[df_ac_nae["n_ae"] >= 5]["acid"].unique().tolist()
# isinメソッドを用い、acidがacidsに含まれる行のみ抽出
df_ae = df_ae[df_ae["acid"].isin(acids)].reset_index(drop=True)
Hide code cell content
# dateをdatetime型に変換し、yearを取得して列に格納
df_ae["date"] = pd.to_datetime(df_ae["date"])
df_ae["year"] = df_ae["date"].dt.year

# アニメ作品数が極端に少ない1989年以前と、データが途中で打ち切られている2017年は除外
df_ae = df_ae[(df_ae["year"] >= 1990) & (df_ae["year"] <= 2016)].reset_index(drop=True)

# yearごとにユニークなacidとaeid数を集計し格納
# 全てのyearの組合せてに対して行が存在するようアップサンプリング
df_an = df_ae.groupby(["year"])[["acid", "aeid"]].nunique().reset_index()
df_an = upsample_yearly_data(df_an)

# 可視化用にカラム名を変更
df_an = df_an.rename(
    columns={"year": "放送年", "acid": "アニメ作品数", "aeid": "アニメ各話数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
放送年 アニメ作品数 アニメ各話数
0 1990 17 350
1 1991 52 1749
2 1992 64 1944
3 1993 54 1741
4 1994 59 1919
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../data/an/output/08/connected/an.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'アニメ作品数'、y軸には'アニメ各話数'、テキストと配色には'発売年'を設定
fig = create_connectedplot(
    df_an, x="アニメ作品数", y="アニメ各話数", text="放送年", color="放送年"
)

# 連結散布図を表示
show_fig(fig)

上図は、年間アニメ作品数とアニメ各話数の推移を表した連結散布図です。マーカーの色が暗いほど古く、明るいほど新しい年を表します。アニメ作品は合計5話以上放送されたものに限定して可視化しています。

1995年に始まり2006年頃に頂点を迎えたとされる第三次アニメブーム[信之, 2022]の影響か、1997年頃から一気にアニメ作品数とアニメ各話数が増えていることがわかります。 横軸にアニメ作品数、縦軸にアニメ各話数を取っていますので、原点\((0, 0)\)からマーカー\((x, y)\)に対して引いた直線の傾き\(\frac{y}{x}\)アニメ作品あたりの平均各話数 に相当します。 加えて、第三次アニメブームは、1クール作品の多い深夜アニメを中心としたものです。 以上から、1995年以降、原点とマーカーを結んだ直線の傾きが浅くなる(つまり、図中右下にマーカーが移動する)と想定できます。

上記を確かめるために、第三次アニメブーム直前の1994年のマーカーに対する比例直線を補助線として追加しましょう。

Hide code cell content
# 1994年時点のアニメ作品あたりの平均アニメ各話数
df_tmp = df_an[df_an["放送年"] == 1994].reset_index(drop=True)
ratio = (df_tmp["アニメ各話数"] / df_tmp["アニメ作品数"]).values[0]
Hide code cell source
# 直線y=ratio * xの最小値と最大値を規定
# 最小値は0、最大値は連結散布図の最大値の1.1倍
x_line = [0, df_an["アニメ作品数"].max() * 1.1]
y_line = [0, x_line[1] * ratio]

# 2点を使ってy = xの直線を追加
fig.add_trace(
    go.Scatter(
        x=x_line,
        y=y_line,
        mode="lines",
        line={"color": "red", "dash": "dot"},
        name=f"y={ratio:0.4}x(1994年の各話数/作品数比率の延長)",
    )
)

# 凡例の位置を図の右上に固定
# yanchorとxanchorは凡例の基準点(top:上部、right:右端)を指定
# yとxはその基準点の位置を指定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))

# 散布図を表示
show_fig(fig)

上図は、一年間の合計アニメ作品数と合計アニメ各話数の推移を表した連結散布図です。マーカーの色が暗いほど古く、明るいほど新しい年を表します。アニメ作品は合計5話以上放送されたものに限定して可視化しています。 図中の赤い点線は、原点と1994年のアニメ作品数とアニメ各話数を通るように引いた補助線であり、 回帰直線ではありません [3]

まず、補助線の傾きから、1994年時点ではアニメ作品あたり平均約\(32.53\)話が放送されていたことがわかります。 また、想定通り1995年以降は補助線より右下にマーカーが分布しているため、作品数あたりの各話数が少なくなったことがわかります。

ゲームデータ#

ゲームパッケージに関する量的変数を例に、可視化手法を説明します。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Hide code cell content
# 'date'列を日付型に変換
df_pkg_pf["date"] = pd.to_datetime(df_pkg_pf["date"])

# 新しい列 'year' を追加して、'date'列から年を抽出
df_pkg_pf["year"] = df_pkg_pf["date"].dt.year

# 新しい列 'weekday' を追加して、'date'列から曜日を抽出
df_pkg_pf["weekday"] = df_pkg_pf["date"].dt.weekday
Hide code cell content
# 'year'でグループ化し、'pkgid', 'publisher', 'pfname' のユニークな値の数を集計
# 'reset_index()'を使って、集計後にも 'year' 列をデータフレームに保持する
df_gm = (
    df_pkg_pf.groupby("year")[["pkgid", "publisher", "pfname"]].nunique().reset_index()
)

# 2017年までを可視化対象として指定
df_gm = df_gm[df_gm["year"] <= 2017].reset_index(drop=True)

# 'rename'メソッドを使って列名を変更
df_gm = df_gm.rename(
    columns={
        "year": "発売年",
        "pkgid": "ゲームパッケージ数",
        "publisher": "ゲームパブリッシャー数",
        "pfname": "ゲームプラットフォーム数",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
発売年 ゲームパッケージ数 ゲームパブリッシャー数 ゲームプラットフォーム数
0 1982 12 2 1
1 1983 11 4 2
2 1984 30 12 5
3 1985 59 34 7
4 1986 57 31 5
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../data/gm/output/08/connected/gm.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'ゲームパッケージ数'、y軸には'ゲームパブリッシャー数'、テキストと配色には'発売年'を設定
fig = create_connectedplot(
    df_gm,
    x="ゲームパッケージ数",
    y="ゲームパブリッシャー数",
    text="発売年",
    color="発売年",
)

# 連結散布図を表示
show_fig(fig)

上図は、一年間に発売された合計パッケージ数と合計パブリッシャー数の推移を表現した連結散布図です。 マーカーの色が暗いほど古く、明るいほど新しい年を表します。

1994年頃からパッケージ数と比較して急激にパブリッシャー数が増加していることがわかります。 1995年から1998年はプレイステーションセガサターン、そしてNINTENDO64が競合しており、 家庭用ゲームの市場規模は史上最大の6000億円近くに到達して[友介, 2020]いました。

パブリッシャー数は1999年ごろピークを迎え、それ以降2005年まで減少しました。 この頃は各ゲームメーカーがポストプレイステーションの座をめぐり争っていた時代であり、結果としてプレイステーション2が圧倒的な勝者となりました。 プレイステーション22002年に日本国内で1000万台を出荷し、2005年には世界での出荷台数1億台を達成[友介, 2020]しました。 このような背景から、ゲームパッケージ数は高止まりしたまま、パブリッシャー数が減少したことに違和感はありません。

2005年以降、パッケージ数の増加とともにパブリッシャー数は微増しますが、1999年ほどの増加を見せることはなかったことがわかります。

それでは、プラットフォーム別にパッケージ数とパブリッシャー数の推移を可視化するとどうなるのでしょうか? 前述したプレイステーションシリーズを有する、ソニー[4]のゲームプラットフォームに対して可視化してみましょう。

Hide code cell content
# 'year'と'pfname'でグループ化し、'pkgid'と'publisher'のユニークな値の数を集計
# 'reset_index()'を使って、集計後も 'year' と 'pfname' 列をデータフレームに保持
df_gm2 = (
    df_pkg_pf.groupby(["year", "pfname"])[["pkgid", "publisher"]]
    .nunique()
    .reset_index()
)

# 2017年までを可視化対象として指定
df_gm2 = df_gm2[df_gm2["year"] <= 2017].reset_index(drop=True)

# 'PF2MK'のキーに含まれる 'pfname' のみを保持するためにフィルタリング
# 'isin()' 関数を使って、条件に合う行のみを選択
df_gm2 = df_gm2[df_gm2["pfname"].isin(PF2MK.keys())].reset_index(drop=True)
Hide code cell content
# 'upsample_yearly_data'関数を使って、年ごとのデータをアップサンプリング
df_gm2 = upsample_yearly_data(df_gm2, "pfname")

# 'map'関数を使って、'pfname'列の値を 'PF2MK' の値に変換し、新しい列 'maker' を作成
df_gm2["maker"] = df_gm2["pfname"].map(PF2MK)

# 可視化用にカラム名をわかりやすいものに変更
df_gm2 = df_gm2.rename(
    columns={
        "pfname": "ゲームプラットフォーム名",
        "pkgid": "ゲームパッケージ数",
        "publisher": "ゲームパブリッシャー数",
        "maker": "メーカー名",
        "year": "発売年",
    }
)
Hide code cell content
# 可視化対象のDataFrameを表示
df_gm2.head()
発売年 ゲームプラットフォーム名 ゲームパッケージ数 ゲームパブリッシャー数 メーカー名
0 1983 ファミリーコンピュータ 7 1 任天堂
1 1983 SC-3000 0 0 セガ
2 1983 SG-1000 0 0 セガ
3 1983 SEGAマーク3 0 0 セガ
4 1983 セガ・マスターシステム 0 0 セガ
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm2, DIR_OUT_GM, "gm2")
DataFrame is saved as '../../data/gm/output/08/connected/gm2.csv'.
Hide code cell content
# メーカー名がソニーのものに絞って可視化
maker = "ソニー"
df_gm2_sony = df_gm2[df_gm2["メーカー名"] == maker].reset_index(drop=True)
Hide code cell content
# 可視化対象のDataFrameを表示
df_gm2_sony.head()
発売年 ゲームプラットフォーム名 ゲームパッケージ数 ゲームパブリッシャー数 メーカー名
0 1983 プレイステーション 0 0 ソニー
1 1983 プレイステーション2 0 0 ソニー
2 1983 プレイステーション・ポータブル 0 0 ソニー
3 1983 ゲームアーカイブス 0 0 ソニー
4 1983 プレイステーション3 0 0 ソニー
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm2_sony, DIR_OUT_GM, "gm2_sony")
DataFrame is saved as '../../data/gm/output/08/connected/gm2_sony.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'ゲームパッケージ数'、y軸には'ゲームパブリッシャー数'、テキストと配色には'発売年'を設定
# 'ゲームプラットフォーム名'ごとにファセットを分けて3列で表示
fig = create_connectedplot(
    df_gm2_sony,
    x="ゲームパッケージ数",
    y="ゲームパブリッシャー数",
    facet_col="ゲームプラットフォーム名",
    facet_col_wrap=3,
    text="発売年",
    color="発売年",
    height=600,
)

# ファセット(プラットフォームごとの連結散布図)のタイトルを簡潔にする処理
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# グラフを表示
show_fig(fig)

上図は、一年間に発売された合計パッケージ数と合計パブリッシャー数の推移を表現した連結散布図を、ゲームプラットフォームごとに可視化したものです。 マーカーの色が暗いほど古く、明るいほど新しい年を表します。

プレイステーションプレイステーション2プレイステーション・ポータブル、そしてプレイステーション3で顕著ですが、 増加も減少もパブリッシャー数が先行する ため、原点を出て右回りの投げ縄形状を描いて原点に戻るような軌跡となることがわかります。